<?php

vendor('str');
vendor('ar');

class json
{
	private $quote;
	private $insertInto;

    /**
	 * @param string/str[optional] $insert_into Куда будет отображаться:
	 *     js [default],
	 *     htmlScriptElement,
	 *     xmlElement,
	 *     xmlAttribute,
	 *     xmlComment,
	 *     xmlCData
	 * @param string/str $quote («'» (default) или «"»)
     */
	private function __construct($insertInto = null, $quote = null)
	{
        if (!isset($insertInto)) {
            $insertInto = "js";
        } else {
    	    $insertInto = str($insertInto)->toString();
    	    if (! in_array($insertInto, array('js', 'htmlScriptElement', 'xmlElement', 'xmlAttribute', 'xmlComment', 'xmlCData'))) {
    	        throw new ArgException();
    	    }
        }
        $this->insertInto = $insertInto;

        if (!isset($quote)) {
            $quote = "'";
        } else {
    	    $quote = str($quote)->toString();
    	    if (! in_array($quote, array('"', "'"))) {
    	        throw new ArgException();
    	    }
        }
        $this->quote = $quote;
	}

	static function draw($value, $quote = null)
	{
        $instance = new self('js', $quote);

        return $instance->drawJSON($value);
	}

	static function drawToScriptTag($value, $quote = null)
	{
        $instance = new self('htmlScriptElement', $quote);

        return $instance->drawJSON($value);
	}

	static function drawToAttribute($value, $quote = null)
	{
        $instance = new self('xmlAttribute', $quote);

        return $instance->drawJSON($value);
	}

	static function drawToXML($value, $quote = null)
	{
        $instance = new self('xmlElement', $quote);

        return $instance->drawJSON($value);
	}

	static function drawToComment($value, $quote = null)
	{
        $instance = new self('xmlComment', $quote);

        return $instance->drawJSON($value);
	}

	static function drawToCData($value, $quote = null)
	{
        $instance = new self('xmlCData', $quote);

        return $instance->drawJSON($value);
	}

	private function drawJSON($value)
	{
        switch (gettype($value)) {

        case 'boolean':
            return $this->drawJSON_bool($value);

        case 'integer':
            return $this->drawJSON_int($value);

        case 'double':
            return $this->drawJSON_float($value);

        case 'string':
            return $this->drawJSON_string($value, $this->quote);

        case 'array':
            if (ar::isVector($value)) {
                return $this->drawJSON_vector($value);
            } else {
                return $this->drawJSON_assoc($value);
            }

        case 'NULL':
            return $this->drawJSON_null();

        default:
            throw new ArgException();
        }
	}

	/**
	 * Выводит массив.
	 *
	 * @param vector $value
	 * @return str
	 */
	private function drawJSON_vector($value)
	{
        $pairs = array();
        foreach ($value as $v) {
            if (ar::isVector($v)) {
                throw new ArgException();
            }
            $pairs[] = str($this->drawJSON($v));
        }

        return str("[")->append(str::implode(",", $pairs))->append("]");
	}

	/**
	 * Выводит целое число.
	 *
	 * @param int $value
	 * @return str
	 */
	private function drawJSON_int($value)
	{
        return new str(strval($value));
	}

	/**
	 * Выводит действительное число.
	 *
	 * @param float $value
	 * @return str
	 */
	private function drawJSON_float($value)
	{
        return new str(strval($value));
	}

	/**
	 * Выводит bool.
	 *
	 * @param bool $value
	 * @return str
	 */
	private function drawJSON_bool($value)
	{
        return new str($value ? "true" : "false");
	}

	/**
	 * Выводит null.
	 *
	 * @param null $value
	 * @return str
	 */
	private function drawJSON_null()
	{
        return new str('null');
	}

	/**
	 * Выводит ассоциативный массив.
	 *
	 * @param assoc $value
	 * @return str
	 */
	private function drawJSON_assoc($value)
	{
        $pairs = array();
        foreach ($value as $k => $v) {
            $pairs[] = str($this->drawJSON_string($k))->append(":")->append($this->drawJSON($v));
        }

        return str("{")->append(str::implode(",", $pairs))->append("}");
	}

	/**
	 * Выводит шестнадцатеричное представление символа (код должен быть 0—255).
	 *
	 * @param string/str $value
	 * @return str
	 */
	private static function drawHexChar($char)
	{
	    $code = str($char)->charCodeAt(0);
	    $codes = array();
	    $codes[] = ($code >> 8) % 16;
	    $codes[] = $code % 16;
	    $result = new str("");
	    foreach ($codes as $code) {
	        switch ($code) {

            case 0:
                $result->append("0");
                break;

            case 1:
                $result->append("1");
                break;

            case 2:
                $result->append("2");
                break;

            case 3:
                $result->append("3");
                break;

            case 4:
                $result->append("4");
                break;

            case 5:
                $result->append("5");
                break;

            case 6:
                $result->append("6");
                break;

            case 7:
                $result->append("7");
                break;

            case 8:
                $result->append("8");
                break;

            case 9:
                $result->append("9");
                break;

            case 10:
                $result->append("A");
                break;

            case 11:
                $result->append("B");
                break;

            case 12:
                $result->append("C");
                break;

            case 13:
                $result->append("D");
                break;

            case 14:
                $result->append("E");
                break;

            case 15:
                $result->append("F");
                break;
	        }
	    }

        return $result;
	}

	/**
	 * Выводит строку.
	 *
	 * @param string/str $value
	 * @param string/str $quote («'» или «"»)
	 * @return str
	 */
	private function drawJSON_string($value, $quote = null)
	{
	    /**
	     * Данные эскейп-последовательности не учитывают последующие эскейп-последовательности
	     * для встраиваемых контейнеров:
	     *     — Для xmlElement будут спрятаны «<» и «&» (возможно «>»).
	     *     — Для xmlAttribute будут спрятаны «<» и «&» и «"» (и/или «'») (возможно «>»).
	     * htmlScriptElement — этот метод сделан специально для HTML-элемента «script».
	     * Т. к. его содержимое не будет эскейпится при встраивании в HTML.
	     * Данный метод пригоден и для XHTML, т. к. не будет содержать запрещённых символов
	     * для содержимого XML-элемента.
	     *
	     * В А Ж Н О: Нельза указать эскейп последовательность для символа, чей код превышает 0xFFFE!
	     */
	    static $mustEscapedChars = array(
	       0x00 => '\\0',
	       0x08 => '\\b',
	       0x09 => '\\t',
	       0x0A => '\\n',
	       0x0B => '\\v',
	       0x0C => '\\f',
	       0x0D => '\\r',
	       0x5C => '\\\\',
	       0x2028 => '\u2028',
	       0x2029 => '\u2029',
	    );
	    static $escaped_chars = array(
	       'htmlScriptElement' => array(/*'<'*/0x3C => '\\x3C', /*'&'*/0x26 => '\\x26'),
	    );
	    static $escaped_sequences = array(
	       'xmlComment' => array('--' => '-\\x2D'),
	       'xmlCData' => array(']]>' => ']]\\x3E'),
	    );

	    $value = str($value);

        if (!isset($quote)) {
            $quote = $this->quote;
        } else {
    	    $quote = str($quote)->toString();
    	    if (! in_array($quote, array('"', "'"))) {
    	        throw new ArgException();
    	    }
        }

	    $escapedChars = isset($escaped_chars[$this->insertInto]) ? $escaped_chars[$this->insertInto] : array();
	    foreach ($mustEscapedChars as $k => $v) {
	        $escapedChars[$k] = $v;
	    }
	    $escapedChars[str::getCode($quote)] = '\\'.$quote;
	    $escapedSequences = isset($escaped_sequences[$this->insertInto]) ? $escaped_sequences[$this->insertInto] : array();

    	$result = new str("");
    	$result->append($quote);
    	$result->append($this->_drawStringContentRecursive($value, $escapedChars, $escapedSequences));
    	$result->append($quote);

        return $result;
	}

	private function _drawStringContent($value, $escapedChars)
	{
    	$result = new str("");
        for ($i = 0; $i < $value->length; $i++) {
            $charCode = $value->charCodeAt($i);
            if (isset($escapedChars[$charCode])) {
                if ($charCode == 0) {
                    $nextCode = ($i + 1) < $value->length ? $value->charCodeAt($i + 1) : null;
                    if (isset($nextCode) && (($nextCode >= /*'0'*/0x30) && ($nextCode <= /*'9'*/0x39))) {
                        $result->append("\\x00");
                    } else {
                        $result->append($escapedChars[$charCode]);
                    }
                } else {
                    $result->append($escapedChars[$charCode]);
                }
            } elseif ($charCode < 0x20) {
                $result->append("\\x");
                $result->append(self::drawHexChar($charCode));
            } else {
                $result->append($value->toChar($i));
            }
        }

        return $result;
	}

	private function _drawStringContentRecursive($value, $escapedChars, $escapedSequences)
	{
	    /* @var $value istr */
        if (count($escapedSequences)) {
            $replacment = reset($escapedSequences);
            $sequence = key($escapedSequences);
            unset($escapedSequences[$sequence]);
        	$parts = array();
        	foreach ($value->split($sequence) as $part) {
                $parts[] = $this->{__FUNCTION__}($part, $escapedChars, $escapedSequences);
        	}
            return str::implode($replacment, $parts);
        } else {
            return $this->_drawStringContent($value, $escapedChars);
        }
	}

}

?>